Цель:¶
- Проведение А/В - теста,
- Оценка результатов А/В - теста.
Задачи:¶
- Импорт библиотек,
- Импорт и чтение датасетов,
- Предобработка датасетов,
- Проведение исследовательского анализа данных,
- Провести А/В - тестирование и оченить его результаты,
- Сформулировать общие выводы по результатам исследования.
ПОСТАНОВКА ЗАДАЧИ: Необходимо провести оценку результатов А/В - теста. Имеется датасет с действиями пользователей, техническое задание и несколько вспомогательных датасетов.
НЕОБХОДИМО:
ТЕХНИЧЕСКОЕ ЗАДАНИЕ:
ПЛАН РАБОТЫ (согласно технического задания):
Импортируем библиотеки и заранее избавляемся от возможных предупреждений системы
# Импорт библиотек
import pandas as pd
import plotly.express as px
import datetime
import numpy as np
from scipy import stats
import seaborn as sns
import matplotlib.pyplot as plt
from plotly import graph_objects as go
import math as mth
import warnings
# Убираем предупреждения системы
warnings.filterwarnings('ignore')
Импортируем и читаем датасеты
# Импорт датасетов
market=pd.read_csv('/Users/denisbogomolov/Documents/Data_Analysis/ab_project_marketing_events.csv')
users=pd.read_csv('/Users/denisbogomolov/Documents/Data_Analysis/final_ab_new_users.csv')
events=pd.read_csv('/Users/denisbogomolov/Documents/Data_Analysis/final_ab_events.csv')
participants=pd.read_csv('/Users/denisbogomolov/Documents/Data_Analysis/final_ab_participants.csv')
Читаем первый датасет и проверяем его на наличие пропусков и дубликатов
# Выводим датасет
display(market.sample(10))
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 7 | Labor day (May 1st) Ads Campaign | EU, CIS, APAC | 2020-05-01 | 2020-05-03 |
| 13 | Chinese Moon Festival | APAC | 2020-10-01 | 2020-10-07 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| 8 | International Women's Day Promo | EU, CIS, APAC | 2020-03-08 | 2020-03-10 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 5 | Black Friday Ads Campaign | EU, CIS, APAC, N.America | 2020-11-26 | 2020-12-01 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 9 | Victory Day CIS (May 9th) Event | CIS | 2020-05-09 | 2020-05-11 |
# Используем функцию - комбайн из предыдущих проектов
def info (dataframe):
"""Это функция - комбайн для извлечения информации из "сырого" датасета
Последовательно выводим общую информацию;
Выводим количество дубликатов (если есть);
Выводим количество NaN
"""
print (dataframe.info())
print()
print ('Количество duplicates:',dataframe.duplicated().sum())
print()
print('Количество NaN:')
print(dataframe.isnull().sum())
# Выводим результат
info(market)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes None Количество duplicates: 0 Количество NaN: name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64
ВАЖНО! В датасете отсутствуют пропуски и дубликаты.
Изменяем тип данных в столбцах start_dt и finish_dt
# Изменение типа данных
market['start_dt'] = pd.to_datetime(market['start_dt'])
market['finish_dt'] = pd.to_datetime(market['finish_dt'])
Читаем второй датасет и проверяем его на наличие пропусков и дубликатов
# Выводим датасет
display(users.sample(10))
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 50149 | E635116E30A2E293 | 2020-12-19 | N.America | Android |
| 11097 | 6E0FDA9BC8A79F2C | 2020-12-14 | EU | iPhone |
| 56274 | 4D7BBDF1A0297156 | 2020-12-13 | N.America | iPhone |
| 5866 | 46A7D0A0D429AC0D | 2020-12-14 | N.America | PC |
| 37775 | 808A0F575399A895 | 2020-12-17 | EU | iPhone |
| 55176 | D1E339DEA2BCDCDD | 2020-12-13 | EU | iPhone |
| 45485 | B0CD4243DDCC5C4F | 2020-12-12 | EU | Mac |
| 8929 | C899D412CD93FE31 | 2020-12-14 | EU | Android |
| 54044 | 31EE008CEACA5F97 | 2020-12-13 | EU | Mac |
| 14682 | CF9534CBCAEC5375 | 2020-12-21 | APAC | Android |
# Выводим результат
info(users)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB None Количество duplicates: 0 Количество NaN: user_id 0 first_date 0 region 0 device 0 dtype: int64
ВАЖНО! В датасете отсутствуют пропуски и дубликаты.
Изменим тип данных в столбцe first_date и выведем уникальные значения в столбце region
# Изменение типа данных
users['first_date'] = pd.to_datetime(users['first_date'])
# Уникальные значения в region
users.region.unique()
array(['EU', 'N.America', 'APAC', 'CIS'], dtype=object)
Читаем третий датасет и проверяем его на наличие пропусков и дубликатов
# Выводим датасет
display(events.sample(10))
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 60300 | 0ADB06F14F6653E0 | 2020-12-28 03:36:45 | purchase | 9.99 |
| 109338 | 12A9E3879CE147DC | 2020-12-22 11:56:40 | product_cart | NaN |
| 351316 | 5479EA1ABE33313B | 2020-12-19 10:03:53 | login | NaN |
| 127178 | 24FBB4C98EFBC0B7 | 2020-12-07 18:26:15 | product_page | NaN |
| 343816 | AF06C98275C222D5 | 2020-12-18 02:57:11 | login | NaN |
| 87258 | 903CF08E400A367F | 2020-12-17 22:19:28 | product_cart | NaN |
| 16293 | 3E286601D309601C | 2020-12-14 15:50:27 | purchase | 4.99 |
| 383330 | 61FA996DE5EACF84 | 2020-12-22 04:55:01 | login | NaN |
| 152103 | B8A21F826E2E3AF8 | 2020-12-13 05:42:02 | product_page | NaN |
| 420093 | 29CB93B1915AA4CA | 2020-12-26 04:30:01 | login | NaN |
# Выводим результат
info(events)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB None Количество duplicates: 0 Количество NaN: user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64
ВАЖНО! В датасете отсутствуют дубликаты, однако имеются многочисленные пропуски в столбце details.
Изменяем тип данных в столбце event_dt
# Изменение типа данных
events['event_dt'] = pd.to_datetime(events['event_dt'])
Анализируем природу многочисленных пропусков
# Выводим уникальные значения столбца details
events.details.unique()
array([ 99.99, 9.99, 4.99, 499.99, nan])
ВАЖНО! Нам известно, что в данном столбце хранятся дополнительные данные о покупках - стоимость покупок в долларах. Отсюда можно сделать вывод, что NaN - это, возможно, отсутствие самого факта покупки.
Читаем четвертый датасет и проверяем его на наличие пропусков и дубликатов
# Выводим датасет
display(participants.sample(10))
| user_id | group | ab_test | |
|---|---|---|---|
| 9193 | 81DE6EC109773756 | B | interface_eu_test |
| 1676 | AC07099C53E86F19 | A | recommender_system_test |
| 271 | C1F5E87B19464519 | B | recommender_system_test |
| 1909 | B5006488F35ACDDB | A | recommender_system_test |
| 13278 | 587B5FAC319A5B8A | A | interface_eu_test |
| 323 | 23199531160930C6 | A | recommender_system_test |
| 5706 | B582D211C1BE1FFD | B | recommender_system_test |
| 415 | D5677961ADDD8C4F | A | recommender_system_test |
| 9976 | C52AB7419534CE29 | B | interface_eu_test |
| 5153 | DE20AA504779785D | A | recommender_system_test |
# Выводим результат
info(participants)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB None Количество duplicates: 0 Количество NaN: user_id 0 group 0 ab_test 0 dtype: int64
ВАЖНО! В датасете отсутствуют пропуски и дубликаты.
Выведем уникальные значения в столбцах group и ab_test
# Выводим уникальные значения столбца group
participants.group.unique()
array(['A', 'B'], dtype=object)
# Выводим уникальные значения столбца ab_test
participants.ab_test.unique()
array(['recommender_system_test', 'interface_eu_test'], dtype=object)
ЭТАП 1. ВЫВОДЫ:
Рассмотрим распределение пользователей по двум тестам
# Выводим результаты распределения
participants.groupby('ab_test').nunique()
| user_id | group | |
|---|---|---|
| ab_test | ||
| interface_eu_test | 11567 | 2 |
| recommender_system_test | 6701 | 2 |
ВАЖНО!
Проверим факт участия пользователей в обоих тестах
# Выводим результат
both_tests=participants.groupby('user_id')['ab_test'].nunique().reset_index().query('ab_test > 1').count().iloc[0]
print('Количество пользователей, принимавших участие в обоих тестах:', both_tests)
Количество пользователей, принимавших участие в обоих тестах: 1602
# Проверим разницу еще раз
print('Уникальное количество пользователей: ', participants.user_id.nunique())
print('Количество записей в таблице: ', len(participants))
print('Разница:',len(participants) - participants.user_id.nunique())
Уникальное количество пользователей: 16666 Количество записей в таблице: 18268 Разница: 1602
ВАЖНО! 1602 пользователя принимало участие в обоих тестах.
ВАЖНО!
В теории, разницу в 1602 пользователя, равно как и второй тест (interface_eu_test) рекомендуется удалять для того, чтобы эти данные не искажали результаты анализа. На практике же это не всегда уместно, тем более, что формирование абсолютно "чистых" данных - крайне маловероятно. Мне кажется, что удаление данных, тем более, напрямую связанных с пользователями - это самя крайняя мера и в конкретном случае удалять эти данные не стоит.
Проверим распределение пользователей, которые участвовали в другом тесте в группах теста recommender_system_test
# Заводим новую переменную
rst_users = participants.query('ab_test == "recommender_system_test"')['user_id']
participants[participants['user_id'].isin(rst_users)].query('ab_test != "recommender_system_test"')['group'].value_counts()
A 819 B 783 Name: group, dtype: int64
ВАЖНО! Пользователи, принимавшие участие в другом тесте, распределяются в группах recommender_system_test практически равномерно. Возможно, это не повлияет на результаты анализа...
Оценим количество пользователей данного теста, которые вошли в датасет events
print('Количество пользователей теста, вошедших в датасет "events":'\
,events[events['user_id'].isin(rst_users)]['user_id'].nunique())
Количество пользователей теста, вошедших в датасет "events": 3675
Оценим количество пользователей, которые не проявляли активности с момента регистрации
print('Количество неактивных пользователей:'\
,rst_users.count()-events[events['user_id'].isin(rst_users)]['user_id'].nunique())
Количество неактивных пользователей: 3026
ВАЖНО! 3026 пользователей после регистрации 07.12.2020 не проявляли никакой активности вплоть до 04.01.2021.
Оценим распределение неактивных пользователей по группам в тесте
nonactive = rst_users[~participants['user_id'].isin(events[events['user_id'].isin(rst_users)]['user_id'])]
nonactive_count = participants[participants['user_id'].isin(nonactive)]['group']\
.value_counts().reset_index().rename(columns={'group':'counts'})
nonactive_count['%'] = round(nonactive_count['counts']/nonactive_count.counts.sum()*100,2)
nonactive_count
| index | counts | % | |
|---|---|---|---|
| 0 | B | 2301 | 61.51 |
| 1 | A | 1440 | 38.49 |
ВАЖНО! Неактивных пользователей в группе В 61,5%, а в группе А - 38,5%
Проверим пересечение по пользователям в группах recommender_system_test
# Проверяем пересечение по пользователям в группах
crossing = participants.query('ab_test == "recommender_system_test"').groupby('user_id')['group']\
.nunique().reset_index().query('group > 1')
crossing
| user_id | group |
|---|
ВАЖНО! Пересечений по группам нет.
Посмотрим на численное распределение пользователей по группам
# Выводим распределение
ab_div = participants.query('ab_test == "recommender_system_test"')['group']\
.value_counts().reset_index().rename(columns={'group':'counts'})
ab_div['%'] = round(ab_div['counts']/ab_div.counts.sum()*100,2)
ab_div
| index | counts | % | |
|---|---|---|---|
| 0 | A | 3824 | 57.07 |
| 1 | B | 2877 | 42.93 |
ВАЖНО! В группе А - 57,07% пользователей, в группе В - 42,93% пользователей. Распеределение не является равномерным!
Посмотрим, совпадает ли время регистрации пользователей с условием ТЗ
# Выводим даты регистрации
users.groupby('first_date')['user_id'].count()
first_date 2020-12-07 5595 2020-12-08 3239 2020-12-09 2101 2020-12-10 3076 2020-12-11 2390 2020-12-12 3963 2020-12-13 4691 2020-12-14 5654 2020-12-15 3043 2020-12-16 2110 2020-12-17 3048 2020-12-18 3365 2020-12-19 3617 2020-12-20 4288 2020-12-21 6290 2020-12-22 3083 2020-12-23 2180 Name: user_id, dtype: int64
ВАЖНО! Часть пользователей зарегистировалась после окончания регистрации по условиям ТЗ.
ПРОМЕЖУТОЧНЫЕ ВЫВОДЫ:
Рассмотрим распределение пользователей по регионам из recommender_system_test
# Формируем датасет
ab_users=pd.merge(rst_users,users,on='user_id',how='left')
# Воспользуемся модифицированной функцией из предыдущих проектов для вывода графиков
def plot(data, column, name,rot=None, indent= 300):
'''Функция для построения графиков
график countplot, аргументы:
data - датасет, column - столбец, name - имя графика,
rot - поворот подписей относительно оси Х,
indent - отступ значений от столбца.
'''
fig = plt.figure(figsize=(12,8))
ax = sns.countplot(x = column,
data = data,
order = data[column].value_counts().index,
palette=['orange', 'forestgreen', 'navy', 'red'], ec='black')
sns.despine(bottom=False)
ax.set(xlabel=None)
plt.xticks(rotation=rot)
ax.set_title(name,
fontsize=20, color = 'black', y=1.04)
for i, count in enumerate(data[column].value_counts()):
ax.text(i, count+indent, str(count) + ' (' + str(round(count/data[column].value_counts().sum()*100,2))+' %)',
horizontalalignment='center', fontsize=13, color='black')
plt.show()
# Выведем гистограмму распределения по событиям
plot(ab_users, 'region', 'Распределение регистраций пользователей по регионам')
ВАЖНО! Из 6701 пользователя в recommender_system_test, 94,78% приходится на регион EU.
Проверим, совпадает ли распределение событий для новых пользователей по датам с условием ТЗ
# Выводим через .min() и .max()
print('Начало событий новых пользователей:',events['event_dt'].min())
print('Окончание событий новых пользователей:',events['event_dt'].max())
Начало событий новых пользователей: 2020-12-07 00:00:33 Окончание событий новых пользователей: 2020-12-30 23:36:33
ВАЖНО! Данных по новым событиям практически на пять дней меньше, чем обозначено в ТЗ.
Проверим, какие маркетинговые события попадали на время проведения теста
# Выводим срез методом .query()
market.query('start_dt >="2020-12-07" & finish_dt >="2020-12-30"')
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
ВАЖНО! Событие Christmas & New Year Promo актуально для региона EU. Из-за этого, поведение пользователей может отличаться от привычного и это, в свою очередь, может сказаться на проведении исследования.
Согласно ТЗ, в анализе должны учитываться только 14 первых дней активности пользователей, после регистрации в системе. Исключаем лишние данные
# Исключаем лишние данные, используем .timedelta()
ab_test = events[events['user_id'].isin(rst_users)]
ab_test['date'] = ab_test['event_dt'].dt.date
ab_test['first_date'] = ab_test.groupby('user_id')['date'].transform('min')
ab_test['life'] = (ab_test['date']-ab_test['first_date'])/datetime.timedelta(days=1)
ab_test = ab_test.query('life<15')
Согласно ТЗ, аудитория - 15% новых пользователей из региона EU. Проверяем
# Выводим результат методом .query()
region_per=len(users[users['user_id'].isin(rst_users)][users['region'] == "EU"])\
/len(users.query('first_date <"2020-12-22" and region =="EU"'))*100
print('Новых пользователей из региона EU (%):',region_per)
Новых пользователей из региона EU (%): 15.0
ВАЖНО! Данное условие ТЗ выполняется.
Формируем датасет, состоящий исключительно из пользователей из региона EU
# Выводим
print('Количество пользователей из региона EU:',ab_test.user_id.nunique())
Количество пользователей из региона EU: 3675
Добавим в датасет тестовые группы
# Удаляем событие, используем метод .loc
ab_test=ab_test.loc[(ab_test['event_dt'] < '2020-12-25')]
# Используем метод .merge()
rec_test = participants.query('ab_test == "recommender_system_test"')
ab_test = ab_test.merge(rec_test[['user_id', 'group']], how='left', on='user_id')
# Выведем результат
display(ab_test.sample(10))
| user_id | event_dt | event_name | details | date | first_date | life | group | |
|---|---|---|---|---|---|---|---|---|
| 19622 | B41C0BC88EABA39B | 2020-12-22 20:51:39 | login | NaN | 2020-12-22 | 2020-12-14 | 8.0 | A |
| 3503 | A7E75EB0530CB91A | 2020-12-14 09:41:22 | product_cart | NaN | 2020-12-14 | 2020-12-08 | 6.0 | A |
| 7572 | 9C2240EEE1D4AB2D | 2020-12-15 19:27:17 | product_page | NaN | 2020-12-15 | 2020-12-12 | 3.0 | B |
| 14987 | 1AFB8C3DE876B77F | 2020-12-16 20:49:05 | login | NaN | 2020-12-16 | 2020-12-14 | 2.0 | A |
| 16723 | 9516220E9139017D | 2020-12-18 23:52:51 | login | NaN | 2020-12-18 | 2020-12-18 | 0.0 | A |
| 18133 | 66BBDB594A250B3B | 2020-12-20 23:44:02 | login | NaN | 2020-12-20 | 2020-12-19 | 1.0 | A |
| 14943 | F34FBD4CB9207474 | 2020-12-16 19:56:33 | login | NaN | 2020-12-16 | 2020-12-14 | 2.0 | A |
| 7973 | A1C3D3C6C3CADDC5 | 2020-12-16 04:28:14 | product_page | NaN | 2020-12-16 | 2020-12-11 | 5.0 | A |
| 15672 | 57FD266D6A09BCD2 | 2020-12-17 17:44:25 | login | NaN | 2020-12-17 | 2020-12-15 | 2.0 | A |
| 15630 | 1AA371B503596160 | 2020-12-17 14:59:54 | login | NaN | 2020-12-17 | 2020-12-14 | 3.0 | B |
ПРОМЕЖУТОЧНЫЕ ВЫВОДЫ:
Полученные данные как качественно, так и количественно довольно существенно отличаются от тех, что даны в качестве условий в ТЗ. Это может существенным образом исказить результаты анализа.
Исследуем конверсию. Рассмотрим распределение событий (активности) пользователей. Удалим маркетинговые события, которые могут влиять на поведение пользователей.
# Выводим гистограмму распределения событий по группам
plt.figure(figsize = (20, 10))
plt.grid(False)
plt.title('Распределение событий по тестовым группам', size=15)
plot = sns.countplot(data = ab_test, x = 'event_name', hue = 'group', order = ab_test['event_name']\
.value_counts().index)
for p in plot.patches:
plot.annotate(format(p.get_height(), '.0f'),
(p.get_x() + p.get_width() / 2., p.get_height()),
ha = 'center', va = 'center',
xytext = (0, 9),
textcoords = 'offset points')
plt.ylabel('Количество событий')
plt.xlabel('События')
plt.show()
Подсчитаем количество событий в группах
# Выводим, ипользуем метод .count()
ab_test.groupby('group')['event_name'].count()
group A 16438 B 4789 Name: event_name, dtype: int64
ВАЖНО!
Количество событий в группе А (16438) более, чем в три раза превышает количество событий в группе В (4789).
Интересно, что последовательность событий в группах различна:
По всей видимости, часть пользователей из группы А покупала товар, даже не заглядывая в корзину.
Оценим распределение количества событий, совершаемых пользователями за период 07.12. 2020 - 25.12.2020
#Выделяем дату из столбца event_dt
ab_test['event_dt'] = ab_test['event_dt'].dt.date
ab_test['event_dt'] = pd.to_datetime(ab_test['event_dt'])
# Выводим гистограмму
plot=ab_test.groupby(['event_dt', 'group'])['user_id'].count().reset_index()
plot.index = plot['event_dt']
plt.figure(figsize = (20, 8))
plt.grid(True)
plt.title('Распределение количества событий пользователей по дням в разбивке на группы', size=14)
sns.barplot(data = plot, x = plot.index.strftime('%d.%m.%Y'), y = 'user_id', hue = 'group')
plt.ylabel('Количество событий')
plt.xlabel('Дата')
plt.xticks(rotation = 40)
plt.show()
ВАЖНО!
Теперь посмотрим, сколько событий совершает один пользователь
# Выводим
print('В среднем 1 пользователь совершает {:.0f} событий'\
.format(ab_test.groupby('user_id')['event_name'].count().mean()))
В среднем 1 пользователь совершает 6 событий
ПРОМЕЖУТОЧНЫЕ ВЫВОДЫ:
Приступаем к формированию продуктовой воронки
# Подсчитываем количество зарегистрированных прользователей в группах
rec_test = rec_test.merge(users[['user_id','region']], how='left', on='user_id')
rec_test_EU = rec_test.query('region =="EU"')
login = rec_test_EU[~rec_test_EU['user_id'].isin(events[events['user_id'].isin(rst_users)]['user_id'].unique())]
login_A = len(login.query('group == "A"'))
login_B = len(login.query('group == "B"'))
# Выводим
print('Количество пользователей совершивших login в группе А:',login_A)
Количество пользователей совершивших login в группе А: 1030
# Выводим
print('Количество пользователей совершивших login в группе В:',login_B)
Количество пользователей совершивших login в группе В: 1840
# Проверяем
print('Количество зарегистрированных пользователей в группах в регионе EU:'\
,ab_test.user_id.nunique()+login_A+login_B)
Количество зарегистрированных пользователей в группах в регионе EU: 6545
ВАЖНО!
6545 пользователей - это 3675 активные пользователи из региона EU плюс те, кто совершил операцию login, но дальше активности не проявлял.
# Формируем воронку по группе А
funnel_A = ab_test.query('group == "A"').groupby('event_name')['user_id'].nunique()\
.sort_values(ascending=False).reset_index()
funnel_A.loc[0, 'event_name'] = 'login'
funnel_A.loc[0, 'user_id'] = funnel_A.loc[0, 'user_id'] + login_A
funnel_A = funnel_A.loc[[0,1,3,2]]
funnel_A.style.bar(subset=['user_id'], color='orange')
| event_name | user_id | |
|---|---|---|
| 0 | login | 3777 |
| 1 | product_page | 1780 |
| 3 | product_cart | 824 |
| 2 | purchase | 872 |
# Формируем воронку по группе В
funnel_B = ab_test.query('group == "B"').groupby('event_name')['user_id'].nunique()\
.sort_values(ascending=False).reset_index()
funnel_B.loc[0, 'event_name'] = 'login'
funnel_B.loc[0, 'user_id'] = funnel_B.loc[0, 'user_id'] + login_B
funnel_B = funnel_B.loc[[0,1,3,2]]
funnel_B.style.bar(subset=['user_id'], color='orange')
| event_name | user_id | |
|---|---|---|
| 0 | login | 2767 |
| 1 | product_page | 523 |
| 3 | product_cart | 255 |
| 2 | purchase | 256 |
# Выводим воронку
fig = go.Figure()
fig.add_trace(go.Funnel(name = 'A',
y=funnel_A.event_name,
x=funnel_A.user_id,
textinfo = "value+percent initial",
marker={"color": ['orange', 'forestgreen', 'navy','red']}),
)
fig.add_trace(go.Funnel(name = 'B',
y=funnel_B.event_name,
x=funnel_B.user_id,
textinfo = "value+percent initial",
marker={"color": ['red','navy','forestgreen','orange']}),
)
fig.update_layout(plot_bgcolor='white',
title=dict(text='Воронка событий для групп A и B',
font=dict(color='black'), x=0.55, y=0.87),
)
fig.show()
ВАЖНО!
ЭТАП 2. ВЫВОДЫ:
Выведем еще раз количество пользователей в каждой группе в разрезе событий
# Объявляем переменную
group_events = ab_test.pivot_table(index = 'event_name', columns = 'group', values = 'user_id', aggfunc = 'nunique')\
.sort_values('A', ascending = False)
# Выводим
group_events
| group | A | B |
|---|---|---|
| event_name | ||
| login | 2747 | 927 |
| product_page | 1780 | 523 |
| purchase | 872 | 256 |
| product_cart | 824 | 255 |
ВАЖНО! Размеры групп существенно различны. Разбиение данных по группам неоптимально. В группах присутсвуют исключительно активные пользователи.
При проверке статистической разницы долей применим Z-критерий для сравнения двух независимых выборок. Критический уровень значимости alpha примем за .05. Также применим поправку Бонферрони.
Формулируем Н0 и Н1 гипотезы
ГИПОТЕЗА Н0: Доли двух сравниваемых контрольных групп равны. Между конверсией групп значимая разница отсутствует.
ГИПОТЕЗА Н1: Доли двух сравниваемых контрольных групп различны. Между конверсией групп есть значимая разница.
Проводим множественный тест (три сравнения по: 'product_page', 'product_cart', 'purchase'. Событие login исключаем, поскольку это действие (событие) совершили все пользователи групп). Чтобы исключить вероятность ошибки, необходимо скорректировать alpha методом Бонферрони - поделить уровень значимости на число сравнений.
# Выводим поправку уровеня значимости
alpha = 0.05
alpha_corr = alpha/3
Рассмотрим результаты проверки гипотез
# Используем модифицированную функцию из предыдущих проектов
def z_test(group_1, group_2, eventname, alpha):
"""Функция для проверки разницы долей с помощью z-критерия
аргументы: group_1, group_2 - тестовые группы А и В, eventname - событие, alpha - уровень значимости
"""
group_A = group_events.loc[eventname, group_1]
group_B = group_events.loc[eventname, group_2]
n1 = group_events.iloc[0, 0]
n2 = group_events.iloc[0, 1]
p1 = group_A / n1
p2 = group_B / n2
difference = p1 - p2
p_combined = (group_A + group_B) / (n1 + n2)
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1 / n1 + 1 / n2))
distr = stats.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
alpha_corr
print('Проверка различий конверсий в событии', eventname)
print('p-value: ',p_value)
if (p_value < alpha_corr):
print("Отвергаем нулевую гипотезу: между конверсией групп есть значимая разница.")
else:
print("Не получилось отвергнуть нулевую гипотезу: между конверсией групп значимая разница отсутствует.")
print('')
# Выводим результаты проверки
for eventname in group_events.index:
z_test('A', 'B', eventname, alpha_corr)
Проверка различий конверсий в событии login p-value: nan Не получилось отвергнуть нулевую гипотезу: между конверсией групп значимая разница отсутствует. Проверка различий конверсий в событии product_page p-value: 5.084368080776613e-06 Отвергаем нулевую гипотезу: между конверсией групп есть значимая разница. Проверка различий конверсий в событии purchase p-value: 0.018474632659979617 Не получилось отвергнуть нулевую гипотезу: между конверсией групп значимая разница отсутствует. Проверка различий конверсий в событии product_cart p-value: 0.15034216422194624 Не получилось отвергнуть нулевую гипотезу: между конверсией групп значимая разница отсутствует.
ЭТАП 3 (Вариант 1). ВЫВОДЫ:
Применение z-критерия и сравнение долей двух групп (без учета "молчащих пользователей") показало, что:
# Еще одна функция для проверки разницы долей с помощью z-критерия
def z_test(event_name, alpha):
"""Проверка статистической разницы долей двух выборок
аргументы: event_name - событие, alpha - критический уровень статистической значимости
"""
alpha = alpha
group_A = funnel_A[funnel_A['event_name'] == event_name]['user_id'].sum()
group_B = funnel_B[funnel_B['event_name'] == event_name]['user_id'].sum()
n1 = funnel_A[funnel_A['event_name'] == 'login']['user_id'].sum()
n2 = funnel_B[funnel_B['event_name'] == 'login']['user_id'].sum()
p1 = group_A / n1
p2 = group_B / n2
p_combined = (group_A + group_B) / (n1 + n2)
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined ) * (1 / n1 + 1 / n2))
distr = stats.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('Проверка различий конверсий в событии', event_name)
print('Группа A насчитывает в событии', event_name,
f'{group_A} пользователей,', f'доля совершивших это событие: {p1:.1%}')
print('Группа B насчитывает в событии', event_name,
f'{group_B} пользователей,', f'доля совершивших это событие: {p2:.1%}')
print('p-значение: ', p_value)
if (p_value < alpha):
print("Отвергаем нулевую гипотезу: между конверсией групп есть значимая разница.")
else:
print("Не получилось отвергнуть нулевую гипотезу: между конверсией групп значимая разница отсутствует. ")
print()
for i in ['product_page', 'product_cart', 'purchase']:
z_test(i, alpha_corr)
Проверка различий конверсий в событии product_page Группа A насчитывает в событии product_page 1780 пользователей, доля совершивших это событие: 47.1% Группа B насчитывает в событии product_page 523 пользователей, доля совершивших это событие: 18.9% p-значение: 0.0 Отвергаем нулевую гипотезу: между конверсией групп есть значимая разница. Проверка различий конверсий в событии product_cart Группа A насчитывает в событии product_cart 824 пользователей, доля совершивших это событие: 21.8% Группа B насчитывает в событии product_cart 255 пользователей, доля совершивших это событие: 9.2% p-значение: 0.0 Отвергаем нулевую гипотезу: между конверсией групп есть значимая разница. Проверка различий конверсий в событии purchase Группа A насчитывает в событии purchase 872 пользователей, доля совершивших это событие: 23.1% Группа B насчитывает в событии purchase 256 пользователей, доля совершивших это событие: 9.3% p-значение: 0.0 Отвергаем нулевую гипотезу: между конверсией групп есть значимая разница.
ЭТАП 3 (Вариант 2). ВЫВОДЫ:
Применение z-критерия и сравнение долей двух групп (с учетом "молчащих пользователей") показало, что между конверсией групп по всем событиям ('product_page', 'product_cart' и 'purchase') есть статистически значимая разница. Воронка событий, построенная с учетом "молчащих пользователей" подтверждает выводы проверки с помощью z-критерия.
ВЫВОДЫ ОТНОСИТЕЛЬНО ТЕХНИЧЕСКОГО ЗАДАНИЯ:
РЕКОМЕНДАЦИИ: